﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace Adjunct.Framework.Dependencies
{
	[DebuggerDisplay("Id[{_id}] Depends[{_dependsOn.Count}] Usages[{_usedBy.Count}]")]
	public class DependencyNode
	{
		public const string TerminatorId = "++";

		private readonly List<DependencyNode> _dependsOn = new List<DependencyNode>();
		private readonly List<DependencyNode> _usedBy = new List<DependencyNode>();

		private DependencyNode(string id)
		{
			Id = id;
		}

		public IEnumerable<DependencyNode> DependsOn
		{
			get { return _dependsOn; }
		}

		public int DependsOnCount
		{
			get { return _dependsOn.Count; }
		}

		public string Id { get; private set; }

		public IEnumerable<DependencyNode> UsedBy
		{
			get { return _usedBy; }
		}

		public int UsedByCount
		{
			get { return _usedBy.Count; }
		}

		public DependencyNode AddDependency(string id)
		{
			if (Id == TerminatorId)
			{
				throw new Exception("You cannot add dependencies to the terminator");
			}
			if (Id == id)
			{
				return this;
			}
			DependencyNode dependency = DependsOn.FirstOrDefault(d => d.Id == id);
			if (dependency == null)
			{
				DependencyNode terminator = DependsOn.FirstOrDefault(d => d.Id == TerminatorId);
				if (terminator != null)
				{
					RemoveDependencyInternal(terminator);
					terminator.RemoveUsageInternal(this);
				}
				else
				{
					terminator = FindTerminator();
				}
				dependency = terminator.FindUsageOf(id) ?? terminator.AddUsage(id);
				AddDependencyInternal(dependency);
				dependency.AddUsageInternal(this);
			}
			return dependency;
		}

		public DependencyNode AddUsage(string id)
		{
			if (id == TerminatorId)
			{
				throw new Exception("You cannot add the terminator as a usage.");
			}
			if (Id == id)
			{
				return this;
			}
			DependencyNode usage = UsedBy.FirstOrDefault(d => d.Id == id);
			if (usage == null)
			{
				DependencyNode terminator = FindTerminator();
				usage = terminator.UsedBy.FirstOrDefault(d => d.Id == id);
				if (usage != null)
				{
					usage.RemoveDependencyInternal(terminator);
					terminator.RemoveUsageInternal(usage);
				}
				else
				{
					usage = terminator.FindUsageOf(id) ?? new DependencyNode(id);
				}
				AddUsageInternal(usage);
				usage.AddDependencyInternal(this);
			}
			return usage;
		}

		public DependencyNode FindDependencyOn(string id)
		{
			return this.Traverse(n => n.DependsOn, n => n.Id == id).FirstOrDefault();
		}

		public DependencyNode FindUsageOf(string id)
		{
			return this.Traverse(dn => dn.UsedBy, dn => dn.Id == id).FirstOrDefault();
		}

		public bool HasDependencyOn(string id)
		{
			return this.Traverse(n => n.DependsOn, n => n.Id == id).Any();
		}

		public bool HasDirectDependencyOn(string id)
		{
			return DependsOn.Any(d => d.Id == id);
		}

		public bool IsDirectlyUsedBy(string id)
		{
			return UsedBy.Any(d => d.Id == id);
		}

		public bool IsUsedBy(string id)
		{
			return this.Traverse(dn => dn.UsedBy, dn => dn.Id == id).Any();
		}

		public void MergeInto(DependencyNode target)
		{
			DependencyNode myTerm = FindTerminator();
			DependencyNode targetTerm = target.FindTerminator();
			if (!ReferenceEquals(myTerm, targetTerm))
			{
				throw new Exception("Nodes are not from the same graph and cannot be merged.");
			}
			foreach (DependencyNode usedBy in UsedBy)
			{
				usedBy.RemoveDependencyInternal(this);
				usedBy.AddDependency(target.Id);
			}
			_usedBy.Clear();
			foreach (DependencyNode dependsOn in DependsOn)
			{
				dependsOn.RemoveUsageInternal(this);
				target.AddDependency(dependsOn.Id);
			}
			_dependsOn.Clear();
		}

		private void AddDependencyInternal(DependencyNode node)
		{
			_dependsOn.Add(node);
			_dependsOn.Sort((left, right) => String.CompareOrdinal(left.Id, right.Id));
		}

		private void AddUsageInternal(DependencyNode node)
		{
			_usedBy.Add(node);
			_usedBy.Sort((left, right) => String.CompareOrdinal(left.Id, right.Id));
		}

		private DependencyNode FindTerminator()
		{
			return FindDependencyOn(TerminatorId);
		}

		private void RemoveDependencyInternal(DependencyNode node)
		{
			_dependsOn.Remove(node);
		}

		private void RemoveUsageInternal(DependencyNode node)
		{
			_usedBy.Remove(node);
		}

		public static DependencyNode NewTerminator()
		{
			return new DependencyNode(TerminatorId);
		}
	}
}